BLOG

Record, summarize, and improve.

gem5 原理

GEM5实际上是一个时钟驱动的离散事件模拟器

ISA构建与解析

gem5 指令集描述语言是一种专门为生成 gem5 所需类定义和解码函数而设计的自定义语言。

该语言的正式语法嵌入在解析器的“yacc”部分(在 isa_parser.py 中查找以 p_开头的函数)。解析器的第二个主要组件处理类似 C 的代码规范以提取指令特性;这方面在代码解析部分中介绍。

在最高层次上,指令集描述文件分为两部分:声明部分和解码部分。解码部分指定了解码器的结构,并定义了解码器返回的特定指令。声明部分定义了支持解码器所需的全局信息(类、指令格式、模板等)。更详细文档

decode
decode bitfield {
	format XX {
		0x00: xx({{
format
def format FormatName(arg1, arg2) {{
	[code omitted]
	#header_output -> decoder.hh
	#decoder_output -> decoder.cc
	#decode_block -> decode case语句
	#exec_output -> generic_put_exec.cc
}};
template
def template LoadStoreDeclare {{
	class xx : public xxx
	{
		[code omitted]
	};
}};
output
output header/decoder/exec {{  //header_output,decoder_output,exec_output
	[code omitted]
}};
let 定义共享的python数据结构和函数,用于指令格式中使用
let {{
	[code omitted]
}};
bitfield
def bitfield xx <0:0>;
operand
def operand_types/operands

从decoder.isa中调用format,生成四部分输出:

  • header_output
  • decoder_output
  • exec_output
  • decode_block

分别对应:

header_output  -> decoder-ns.hh.inc    指令class实现
decoder_output -> decoder-ns.cc.inc    指令class构造函数实现,以及指令class声明
exec_output    -> exec-ns.cc.inc       指令的execute、initiateAcc、completeAcc函数实现
decode_block   -> decode-method.cc.inc 构造decoder函数,根据解码构造指令class

inst-constrs.cc
#include "decoder-g.cc.inc"
#include "decoder-g.hh.inc"
#include "decoder-ns.hh.inc"
#include "decoder-ns.cc.inc"

decoder.cc
#include "decoder-g.cc.inc"
#include "decoder-g.hh.inc"
#include "decoder-ns.hh.inc"
#include "decode-method.cc.inc"

generic_cpu_exec.cc
#include "exec-g.cc.inc"
#include "decoder-g.hh.inc"
#include "decoder-ns.hh.inc"
#include "exec-ns.cc.inc"

结构体&流程

Fetch2
- ForwardInstData
- ForwardLineData
- BranchData
- Fetch2ThreadInfo


ISA::readMiscReg => BaseISA::ThreadContext::PCState::VTYPE(vector state)

// xc
ExecContext
- ThreadContext // tc
	- BaseCPU
		- 
	- BaseMMU
	- BaseISA
		- ISA
			- CSRData
			- miscRegFile
	- InstDecoder
		- ExtMatchInst
	- Process
	- Status
	- PCState
		- VTYPE
	

ThreadContext
	|
	V
StaticInst::ExtMachInst

MinorDynInst::StaticInst

ThreadState
- ThreadContext::Status
- BaseCPU
- Process
- ContextID
- ThreadID

SimpleThread : ThreadState,ThreadContext
- BaseISA
- PCStateBase
- BaseHTMCheckpoint
- PCEventQueue
- EventQueue
- System
- BaseMMU
- InstDecoder



RiscvProcess64::initState() -> Process::initState(); -> tc->SimpleThread::activate(); -> baseCpu->activateContext(_threadId); -> MinorCPU::activateContext(ThreadID thread_id)
-> wakeupOnEvent -> pipeline->start();
-> (只有一次)pipeline wakeupFetch -> wakeupFetch -> cpu wakeupOnEvent -> ticked_object start -> pipeline->start();

MinorCPU::startup() -> pipeline wakeupFetch -> wakeupFetch -> ...

event processClockEvent -> evaluate

thread activate -> cpu activateContext -> pipeline wakeupFetch -> wakeupFetch -> cpu wakeupOnEvent -> ticked_object start -> 
event processClockEvent -> evaluate

execute,initiateAcc,completeAcc

下面这三个钩子是 gem5 指令语义里对访存类指令的三种执行路径(或阶段)。一句话版:

  • execute()单阶段阻塞式执行(一次函数调用就把指令所有副作用都做完,包含访存)。给 AtomicSimpleCPU功能仿真(functional) 等“零/常数时延”路径用。
  • initiateAcc()分阶段的第 1 阶段——只发起访存请求(构造 Request/Packet,送到内存系统),不做依赖返回数据的副作用。给 TimingSimpleCPU / O3 / Minor 等“时序/分阶段”路径用。
  • completeAcc()分阶段的第 2 阶段——在内存响应返回后被回调,消化返回数据并提交副作用(写目的寄存器、更新标志位、提交 store 成功/失败等)。

什么时候需要实现?

只跑 Atomic / 功能模式

  • 指令不访问内存:只实现 execute() 即可。
  • 指令访问内存但你只打算AtomicSimpleCPU 或功能模式:实现 execute() 就能工作(它内部会用原子/功能访问把数据一次性读/写好)。

要支持 Timing / O3 / Minor(真实时序)

  • 凡是会访问内存的指令(load/store、原子指令、LL/SC、向量 gather/scatter、访存型自定义扩展、设备 MMIO 等),你必须实现成对的

    initiateAcc()(发请求) + completeAcc()(收响应并提交副作用)。

    否则:

    • 时序 CPU 无法感知访存时延,会当作 0 周期完成;
    • 乱序/重排序、依赖、异常/重放等都会不正确。
gem5 的解码器/代码生成通常会同时给出三套路径的空壳。你在做自定义指令或改语义时,需要把访存逻辑分别填在 execute()(原子/功能)和 initiateAcc()+completeAcc()(时序)两套里。

三者的职责边界(重要!)

execute(xc, trace)

  • 可阻塞:直接调用 xc->readMem/ writeMem(原子/功能访问),拿到数据后当场写目的寄存器 / 写内存 / 更新标志位。
  • 适合:AtomicSimpleCPU,功能访问(调试/校验)。

initiateAcc(xc, trace)

  • 只做两件事
    1. 计算 EA/大小/字节使能、准备数据缓冲(store/AMO),
    2. 通过 xc 发出内存请求(通常隐藏 TLB/端口发送)。
  • 不要在这里修改任何依赖返回数据的架构状态(例如 load 的目的寄存器、比较结果标志等)。
  • 对于分段/多拍(如 vector、按 64B 一组)可以发多次请求,并在动态指令对象里记录 outstanding 计数/状态。

completeAcc(pkt, xc, trace)

  • 在内存系统回包时被 CPU 回调。
  • 这里读取返回数据写目的寄存器更新标志位完成 store-conditional 判定清理 outstanding标注指令完成等。
  • 多拍/多分段时,通常等最后一个响应到达再真正“提交”。

下面结合 gem5 的 MinorCPU 实现(src/cpu/minor/execute.cc)把三类回调的触发时机讲清楚:

1) execute(...) —— 非内存类指令在提交(commit)时调用
  • 当待提交的是非内存指令(ALU、分支、CSR 等),Execute::commit() 会直接构造执行上下文 ExecContext 并调用 staticInst->execute(&context, trace) 完成语义更新、设置谓词、处理可能的 fault,然后做分支处理与统计记账。参见 commitInst 里调用 staticInst->execute(...) 的代码。 Doxygen Gem5

(注:若是内存屏障且 store buffer 没位,或可挂起的 quiesce 指令等,会被延后,不进入 execute 分支。Doxygen Gem5

2) initiateAcc(...) —— 内存指令“发起访问”时调用
  • 内存指令在功能单元(FU)里先计算好有效地址(EA)。当它到达可发起内存访问的时刻(允许早发 allowEarlyMemIssue 时可在到队头前发,否则到队头再发),Execute::executeMemRefInst() 会调用 staticInst->initiateAcc(&context, trace)构造并送出访存请求(进入 LSQ/内存系统),可能产生翻译 fault(TLB fault)等。相关调用见: Doxygen Gem5
    • 代码中:init_fault = inst->staticInst->initiateAcc(&context, inst->traceData);(并对 translation fault 有特殊处理)。 Doxygen Gem5
    • 若启用“早发”(early memory issue)且发生 fault,会被记录下来,等真正轮到提交时再统一处理。 Doxygen Gem5
3) completeAcc(...) —— 内存指令“收到响应/完成访问”时调用
  • 当内存系统返回响应(或访问失败/被抑制)时,handleMemResponse 路径会调用 staticInst->completeAcc(packet, &context, trace) 完成这条内存指令的语义收尾(例如把 load 的数据写回目的寄存器、完成 store 的提交、更新标志位等;如果 packetnullptr 表示失败路径/谓词不通过也走这里)。见:
    • inst->staticInst->completeAcc(packet, &context, inst->traceData);,以及失败时 completeAcc(nullptr, ...)Doxygen Gem5

一张流程小抄(内存指令)
  1. FU 阶段:算 EA。
  2. 发起内存:满足条件时调用 initiateAcc(...),请求进入 LSQ/内存系统(支持 early-issue 时可提前发)。 Doxygen Gem5+1
  3. 返回并完成:收到响应后调用 completeAcc(...),做寄存器/状态更新并使这条内存指令可提交。 Doxygen Gem5
  4. 非内存:不走上面两步,直接在 commit 中调用 execute(...)Doxygen Gem5
关键词定位:
  • completeAcc 调用:execute.cc 中处理内存响应的位置。Doxygen Gem5
  • initiateAcc 调用:execute 内的内存发起逻辑。Doxygen Gem5
  • 非内存走 execute(...)commitInst 中非内存路径。Doxygen Gem5

如果你要在自定义指令里接入访存:

  • 语义拆分到 initiateAcc(发请求)和 completeAcc(拿响应写回/收尾);
  • 非访存语义放在 execute
  • 打开/关闭 allowEarlyMemIssue 会影响“先发访存、后到队头提交”的行为。 Doxygen Gem5

gem5执行流程

gem5 中,m5.main() 函数的执行过程从 Python 开始,然后通过 pybind11 嵌入 Python 到 C++,最终进入 gem5 的核心模拟代码。这一流程通过 pybind11 提供的接口,使得 Python 代码可以调用 C++ 中的实现部分。

libc.so.6!__pthread_kill_implementation(int no_tid, int signo, pthread_t threadid) (pthread_kill.c:44)
libc.so.6!__pthread_kill_internal(int signo, pthread_t threadid) (pthread_kill.c:78)
libc.so.6!__GI___pthread_kill(pthread_t threadid, int signo) (pthread_kill.c:89)
libc.so.6!__GI_raise(int sig) (raise.c:26)
libc.so.6!__GI_abort() (abort.c:79)
gem5::Logger::exit_helper(gem5::Logger * const this) (/data/user61/work_jan/gem5-a4e/src/base/logging.hh:124)
gem5::TCM::CPUSidePort::accessFunctional(gem5::TCM::CPUSidePort * const this, gem5::PacketPtr pkt) (/data/user61/work_jan/gem5-a4e/src/dev/tcm.cc:488)
gem5::TCM::CPUSidePort::recvFunctional(gem5::TCM::CPUSidePort * const this, gem5::PacketPtr pkt) (/data/user61/work_jan/gem5-a4e/src/dev/tcm.cc:204)
gem5::FunctionalRequestProtocol::send(const gem5::FunctionalRequestProtocol * const this, gem5::FunctionalResponseProtocol * peer, gem5::PacketPtr pkt) (/data/user61/work_jan/gem5-a4e/src/mem/protocol/functional.cc:53)
gem5::RequestPort::sendFunctional(const gem5::RequestPort * const this, gem5::PacketPtr pkt) (/data/user61/work_jan/gem5-a4e/src/mem/port.hh:583)
operator()(const struct {...} * const __closure, gem5::PacketPtr pkt) (/data/user61/work_jan/gem5-a4e/src/mem/port_proxy.cc:53)
std::__invoke_impl<void, gem5::PortProxy::PortProxy(const gem5::RequestPort&, gem5::Addr)::<lambda(gem5::PacketPtr)>&, gem5::Packet*>(std::__invoke_other, struct {...} &)(struct {...} & __f) (/usr/include/c++/11/bits/invoke.h:61)
std::__invoke_r<void, gem5::PortProxy::PortProxy(const gem5::RequestPort&, gem5::Addr)::<lambda(gem5::PacketPtr)>&, gem5::Packet*>(struct {...} &)(struct {...} & __fn) (/usr/include/c++/11/bits/invoke.h:111)
std::_Function_handler<void(gem5::Packet*), gem5::PortProxy::PortProxy(const gem5::RequestPort&, gem5::Addr)::<lambda(gem5::PacketPtr)> >::_M_invoke(const std::_Any_data &, gem5::Packet *&&)(const std::_Any_data & __functor,  __args#0) (/usr/include/c++/11/bits/std_function.h:290)
std::function<void (gem5::Packet*)>::operator()(gem5::Packet*) const(const std::function<void(gem5::Packet*)> * const this,  __args#0) (/usr/include/c++/11/bits/std_function.h:590)
gem5::PortProxy::writeBlobPhys(const gem5::PortProxy * const this, gem5::Addr addr, gem5::Request::Flags flags, const void * p, uint64_t size) (/data/user61/work_jan/gem5-a4e/src/mem/port_proxy.cc:96)
gem5::PortProxy::tryWriteBlob(const gem5::PortProxy * const this, gem5::Addr addr, const void * p, uint64_t size) (/data/user61/work_jan/gem5-a4e/src/mem/port_proxy.hh:159)
gem5::PortProxy::writeBlob(const gem5::PortProxy * const this, gem5::Addr addr, const void * p, uint64_t size) (/data/user61/work_jan/gem5-a4e/src/mem/port_proxy.hh:194)
gem5::loader::MemoryImage::writeSegment(const gem5::loader::MemoryImage * const this, const gem5::loader::MemoryImage::Segment & seg, const gem5::PortProxy & proxy) (/data/user61/work_jan/gem5-a4e/src/base/loader/memory_image.cc:43)
gem5::loader::MemoryImage::write(const gem5::loader::MemoryImage * const this, const gem5::PortProxy & proxy) (/data/user61/work_jan/gem5-a4e/src/base/loader/memory_image.cc:56)
gem5::Process::initState(gem5::Process * const this) (/data/user61/work_jan/gem5-a4e/src/sim/process.cc:375)
gem5::RiscvProcess64::initState(gem5::RiscvProcess64 * const this) (/data/user61/work_jan/gem5-a4e/src/arch/riscv/process.cc:102)
pybind11::cpp_function::cpp_function<void, gem5::SimObject, , pybind11::name, pybind11::is_method, pybind11::sibling>(void (gem5::SimObject::*)(), pybind11::name const&, pybind11::is_method const&, pybind11::sibling const&)::{lambda(gem5::SimObject*)#1}::operator()(gem5::SimObject*) const(const struct {...} * const __closure, gem5::SimObject * c) (/data/user61/work_jan/gem5-a4e/ext/pybind11/include/pybind11/pybind11.h:110)
pybind11::detail::argument_loader<gem5::SimObject*>::call_impl<void, pybind11::cpp_function::cpp_function<void, gem5::SimObject, , pybind11::name, pybind11::is_method, pybind11::sibling>(void (gem5::SimObject::*)(), pybind11::name const&, pybind11::is_method const&, pybind11::sibling const&)::{lambda(gem5::SimObject*)#1}&, 0ul, pybind11::detail::void_type>(pybind11::cpp_function::cpp_function<void, gem5::SimObject, , pybind11::name, pybind11::is_method, pybind11::sibling>(void (gem5::SimObject::*)(), pybind11::name const&, pybind11::is_method const&, pybind11::sibling const&)::{lambda(gem5::SimObject*)#1}&, std::integer_sequence<unsigned long, 0ul>, pybind11::detail::void_type&&) &&(pybind11::detail::argument_loader<gem5::SimObject*> * const this, struct {...} & f) (/data/user61/work_jan/gem5-a4e/ext/pybind11/include/pybind11/cast.h:1443)
pybind11::detail::argument_loader<gem5::SimObject*>::call<void, pybind11::detail::void_type, pybind11::cpp_function::cpp_function<void, gem5::SimObject, , pybind11::name, pybind11::is_method, pybind11::sibling>(void (gem5::SimObject::*)(), pybind11::name const&, pybind11::is_method const&, pybind11::sibling const&)::{lambda(gem5::SimObject*)#1}&>(pybind11::cpp_function::cpp_function<void, gem5::SimObject, , pybind11::name, pybind11::is_method, pybind11::sibling>(void (gem5::SimObject::*)(), pybind11::name const&, pybind11::is_method const&, pybind11::sibling const&)::{lambda(gem5::SimObject*)#1}&) &&(pybind11::detail::argument_loader<gem5::SimObject*> * const this, struct {...} & f) (/data/user61/work_jan/gem5-a4e/ext/pybind11/include/pybind11/cast.h:1417)
pybind11::cpp_function::initialize<pybind11::cpp_function::initialize<void, gem5::SimObject, , pybind11::name, pybind11::is_method, pybind11::sibling>(void (gem5::SimObject::*)(), pybind11::name const&, pybind11::is_method const&, pybind11::sibling const&)::{lambda(gem5::SimObject*)#1}, void, gem5::SimObject*, pybind11::name, pybind11::is_method, pybind11::sibling>(pybind11::cpp_function::initialize<void, gem5::SimObject, , pybind11::name, pybind11::is_method, pybind11::sibling>(void (gem5::SimObject::*)(), pybind11::name const&, pybind11::is_method const&, pybind11::sibling const&)::{lambda(gem5::SimObject*)#1}&&, void (*)(gem5::SimObject*), pybind11::name const&, pybind11::is_method const&, pybind11::sibling const&)::{lambda(pybind11::detail::function_call&)#3}::operator()(pybind11::detail::function_call&) const(const struct {...} * const __closure, pybind11::detail::function_call & call) (/data/user61/work_jan/gem5-a4e/ext/pybind11/include/pybind11/pybind11.h:248)
pybind11::cpp_function::initialize<pybind11::cpp_function::initialize<void, gem5::SimObject, , pybind11::name, pybind11::is_method, pybind11::sibling>(void (gem5::SimObject::*)(), pybind11::name const&, pybind11::is_method const&, pybind11::sibling const&)::{lambda(gem5::SimObject*)#1}, void, gem5::SimObject*, pybind11::name, pybind11::is_method, pybind11::sibling>(pybind11::cpp_function::initialize<void, gem5::SimObject, , pybind11::name, pybind11::is_method, pybind11::sibling>(void (gem5::SimObject::*)(), pybind11::name const&, pybind11::is_method const&, pybind11::sibling const&)::{lambda(gem5::SimObject*)#1}&&, void (*)(gem5::SimObject*), pybind11::name const&, pybind11::is_method const&, pybind11::sibling const&)::{lambda(pybind11::detail::function_call&)#3}::_FUN(pybind11::detail::function_call&)() (/data/user61/work_jan/gem5-a4e/ext/pybind11/include/pybind11/pybind11.h:223)
pybind11::cpp_function::dispatcher(PyObject * self, PyObject * args_in, PyObject * kwargs_in) (/data/user61/work_jan/gem5-a4e/ext/pybind11/include/pybind11/pybind11.h:939)
libpython3.10.so.1.0![Unknown/Just-In-Time compiled code] (Unknown Source:0)
libpython3.10.so.1.0!_PyObject_MakeTpCall (Unknown Source:0)
libpython3.10.so.1.0![Unknown/Just-In-Time compiled code] (Unknown Source:0)
libpython3.10.so.1.0!_PyEval_EvalFrameDefault (Unknown Source:0)
libpython3.10.so.1.0![Unknown/Just-In-Time compiled code] (Unknown Source:0)
libpython3.10.so.1.0!_PyEval_EvalFrameDefault (Unknown Source:0)
libpython3.10.so.1.0![Unknown/Just-In-Time compiled code] (Unknown Source:0)
libpython3.10.so.1.0!PyEval_EvalCode (Unknown Source:0)
libpython3.10.so.1.0![Unknown/Just-In-Time compiled code] (Unknown Source:0)
libpython3.10.so.1.0!_PyEval_EvalFrameDefault (Unknown Source:0)
libpython3.10.so.1.0![Unknown/Just-In-Time compiled code] (Unknown Source:0)
pybind11::detail::simple_collector<(pybind11::return_value_policy)1>::call(const pybind11::detail::simple_collector<(pybind11::return_value_policy)1> * const this, PyObject * ptr) (/data/user61/work_jan/gem5-a4e/ext/pybind11/include/pybind11/cast.h:1465)
pybind11::detail::object_api<pybind11::detail::accessor<pybind11::detail::accessor_policies::str_attr> >::operator()<(pybind11::return_value_policy)1>(const pybind11::detail::object_api<pybind11::detail::accessor<pybind11::detail::accessor_policies::str_attr> > * const this) (/data/user61/work_jan/gem5-a4e/ext/pybind11/include/pybind11/cast.h:1635)
main(int argc, char ** argv) (/data/user61/work_jan/gem5-a4e/src/sim/main.cc:87)
pybind11 cpp_function执行流程

我们可以将 pybind11::cpp_function 的工作原理拆解为以下几个关键步骤:

1. 触发阶段:Python 虚拟机 (VM) 到 C API

调用栈底部(main.cc 向上)显示了 Python 解释器的运行过程:

  • PyEval_EvalFrameDefault: 这是 Python 解释器的主循环,正在执行编译后的 Python 字节码。
  • _PyObject_MakeTpCall: 当 Python 脚本调用 system.process.initState() 时,解释器识别出这是一个被封装的 C++ 对象方法,于是通过 Python C API 发起调用。

2. 桥接阶段:cpp_function::dispatcher (最关键的一环)

图中 pybind11.h:939 附近的 dispatcher 是整个 pybind11 的“交通枢纽”:

  • 职责:它是一个符合 Python 调用约定的 static 函数。它接收原始的 PyObject* 参数。
  • 逻辑:它会查找该函数对应的 function_record(存储了 C++ 函数指针、参数类型等信息)。

3. 类型转换阶段:argument_loader

接下来的几行 argument_loader 展示了 pybind11 的元编程魔法:

  • argument_loader<...>::call: 它像一个“翻译官”,利用 C++ 模板遍历 Python 传入的参数元组(tuple)。
  • Type Casting: 它会为每个参数调用 type_caster。例如,将 Python 的字符串转为 std::string,或将 Python 对象引用转为 gem5 内部的 SimObject* 指针。
  • 图中体现:你看到调用栈里有多个 initializecall_impl 的层级,这正是 pybind11 在尝试匹配正确的函数重载(Overload Resolution)。

4. 执行阶段:进入 gem5 核心逻辑

当参数被全部转换并压入 C++ 栈后,cpp_function 会发起最后一击:

  • 调用 Lambda/绑定函数:在图中对应 pybind11::cpp_function::cpp_function<void, gem5::SimObject, ...> 这一行,它解包参数并调用我们在 gem5 导出源码中注册的那个函数。
  • 业务逻辑执行
    1. gem5::RiscvProcess64::initState(): 进入特定架构的初始化。
    2. gem5::Process::initState(): 基类初始化。
    3. gem5::PortProxy::writeBlobPhys(): 最终落到 gem5 内存系统的操作上。

总结工作原理图解

核心公式: Python Call → rightarrow Dispatcher → rightarrow Type Casting → rightarrow C++ → Implementation

以下是 m5.main() 函数的执行流程,以及它如何进入 C++ 代码的详细步骤:

1. C++ 初始化 Python 解释器

gem5 的 C++ 代码中,首先通过 pybind11 初始化 Python 解释器(如下代码所示):

py::scoped_interpreter guard(true, argc, argv);
  • 这行代码创建了一个 py::scoped_interpreter 对象,初始化 Python 环境并设置命令行参数。
  • 这样,gem5 的 C++ 代码可以加载并运行 Python 脚本。
2. 导入并执行 Python 模块中的 main 函数

在 C++ 代码中,Python 模块 m5 被导入并调用 main() 函数:

auto importer = py::module_::import("importer");
importer.attr("install")();

py::module_::import("m5").attr("main")();
  • py::module_::import("m5") 导入 m5 Python 模块。
  • attr("main")() 获取 m5 模块中的 main 函数并立即执行。
  • 此时,m5.main() 开始执行 gem5 的 Python 代码。
3. Python 代码中的 m5.main() 初始化模拟

m5.main() 是 Python 层的主要入口,位于 src/python/m5/main.py 文件中。它负责设置仿真参数并启动仿真核心:

def main():
    # 设置仿真参数和配置
    # 这里进行各种组件的初始化,例如 CPU、内存、缓存等

    # 进入模拟核心并执行
    m5.simulate()
  • main() 函数通常会解析用户配置,创建并连接各个模拟组件(如 CPU、内存、缓存等)。
  • 关键调用是 m5.simulate(),它启动了 gem5 的 C++ 仿真核心。
4. 加载并执行用户的配置脚本
if options.m:
    # 运行指定的 Python 模块
    sys.argv = [options.m[0]] + options.m[1]
    runpy.run_module(options.m[0], run_name="__m5_main__")
else:
    # 运行指定的 Python 脚本文件
    if options.c:
        filedata = options.c[0]
        filecode = compile(filedata, "<string>", "exec")
        sys.argv = ["-c"] + options.c[1]
        scope = {"__name__": "__m5_main__"}
    else:
        filename = sys.argv[0]
        with open(filename, "rb") as fd:
            filedata = fd.read().decode("utf-8")
        filecode = compile(filedata, filename, "exec")
        scope = {"__file__": filename, "__name__": "__m5_main__"}
    exec(filecode, scope)
  • options.moptions.c 是用户通过命令行传递的参数,分别指向用户自定义的 Python 模块或脚本文件。
  • runpy.run_module() 加载并运行模块,exec() 执行脚本文件。
  • 用户的配置脚本通常会包含 m5.simulate() 或类似的调用,以启动仿真。
5. 在用户配置脚本中调用 m5.simulate()
  • 用户配置脚本负责定义仿真参数、构建系统组件(如 CPU、内存、缓存等),并最终调用 m5.simulate() 来启动仿真。
  • 例如,一个典型的 gem5 配置脚本可能如下:
    import m5
    from m5.objects import System, Root
    
    # 设置系统配置
    system = System()
    
    # 配置 CPU、内存、缓存等
    # ...
    
    # 设置根对象并开始仿真
    root = Root(full_system=False, system=system)
    m5.instantiate()
    
    # 开始仿真
    m5.simulate()
  • m5.simulate() 是进入 gem5 C++ 仿真核心的关键入口。

m5 Python 模块中,simulate() 实际上是一个由 pybind11 绑定到 C++ 的函数,它启动了 gem5 的模拟主循环。其定义通常如下:

# src/python/m5/simulate.py
def simulate(*args, **kwargs):
    sim_out = _m5.event.simulate(*args, **kwargs)
  • _m5 是由 pybind11 生成的 C++ 扩展模块,通过它可以直接调用 C++ 层的 simulate 函数。
  • _m5.event.simulate(*args, **kwargs) 调用pybind11/event.cc 中的pybind_init_event 进入了 gem5 的 C++ 仿真核心代码,执行 C++ 模拟循环。
6. 进入 C++ 的 simulate 函数

gem5 的 C++ 代码中,simulate 是仿真主循环的入口点,位于 src/sim/simulate.cc 中:

void simulate(Tick num_ticks) {
    // 主循环,处理事件队列,执行仿真步骤
    while (eventQueue->getNextEventTime() <= num_ticks) {
        eventQueue->serviceOne();
    }
}
  • simulate 函数会进入一个循环,从事件队列中逐个取出事件并执行,直到仿真结束。
  • 在事件驱动的仿真循环中,gem5 根据预设的仿真配置执行各个硬件模块的操作。
7. 仿真结束并返回 Python 层
  • 仿真结束后,C++ 中的 simulate 函数返回。
  • 通过 pybind11,控制权回到 Python 的 m5.simulate() 函数。
  • 最后,m5.main() 执行结束,Python 层完成仿真流程,程序退出。
总结
  • 初始化 Python 解释器:C++ 通过 pybind11 初始化 Python 解释器。
  • 调用 Python m5.main():C++ 调用 m5 模块的 main() 函数,加载并配置模拟环境。
  • 启动仿真核心m5.main() 调用 m5.simulate(),启动 C++ 层的 simulate 函数。
  • 进入 C++ 仿真循环simulate 函数在 C++ 中执行仿真循环,直到所有事件完成。
  • 返回控制:仿真结束后,返回 Python,程序结束。

通过 pybind11,Python 和 C++ 层能够顺畅交互,实现了 gem5 仿真启动、控制和事件驱动的全过程。

1. create() (构造函数阶段)
这是最开始的阶段。Python 层的参数(Params)被传递给 C++。

作用: 调用 C++ 类的构造函数。

主要任务: 分配内存,保存配置参数,初始化成员变量。此时对象之间还没有建立真正的连接(如 Port 还没连上)。

2. regStats() (统计信息注册)
在对象创建后,系统会立即调用这个函数。

作用: 让每个 SimObject 声明它将要记录哪些数据(如 Scalar, Formula, Vector 等)。

重要性: 只有在这里注册过的统计量,最后才会在 stats.txt 中出现。

3. init() (拓扑连接阶段) —— 非常关键
在所有对象都 create 完毕后调用。

作用: 解决对象之间的依赖关系。

主要任务: * Port 绑定:确保 Master Port 和 Slave Port 成功连接。

引用获取:如果一个对象需要指向另一个对象的指针(例如 Cache 需要指向 Memory Controller),通常在这一步完成。

注意: 此时你不能调度事件(Event),因为系统还没准备好。

4. regProbePoints() / regProbeListeners() (探针注册)
这是 gem5 的观测系统。

作用: 允许外部组件“监听”该对象的内部行为(例如监听指令提交、Cache Miss 等),而不改变其原始逻辑。

5. initState() (初态设置)
这是你提到的那个函数。

作用: 仅在**非恢复(Non-checkpoint)**运行模式下执行。

主要任务: 设置内存中的初始数据、初始化 CPU 寄存器状态、加载引导程序(Bootloader)等。

6. loadState() (状态加载)
作用: 与 initState() 互斥。如果你是从 Checkpoint(检查点)恢复运行的,系统会调用这个函数把磁盘上的状态读回内存。

7. startup() (仿真起航) —— 最后的准备
这是在 m5.simulate() 被调用后,仿真循环开始前执行的最后一个函数。

作用: 激活对象。

主要任务: 调度(Schedule)第一个事件。

例如:CPU 会在此时调度它的第一个 tick 事件。如果没有对象在 startup() 里往事件队列里扔东西,仿真会瞬间结束,因为队列是空的。

Param处理

Params初始化,以vlsu为例:

Python Param → SWIG → C++ Params → XXParams.xx

VSLU.py::class VLSU::xx = Param.Int(…) → SWIG → VLSU.hh(auto generate)::VLSUParams.xx = src/python/m5/params.py::class Param.class Int(…) → SimObject

Port中的方法

对比表(Port 中常用接口)

方法 返回值 特性 时序模型 典型使用场景
sendAtomic(PacketPtr pkt) PacketPtr / bool 原子访问,CPU 或模块阻塞直到访问完成 Atomic:不考虑时钟周期,访问瞬时完成 快速仿真,不需要 cycle 精度,建模 RTL 不必关心延时
sendAtomicBackdoor(PacketPtr pkt, MemBackdoorPtr &backdoor) PacketPtr / bool Atomic 访问 + 内存后门(backdoor) Atomic 用于直接访问内存内容,同时可以绕过端口发送路径,用于调试或直接读写内存
sendFunctional(PacketPtr pkt) const bool 功能性访问,非阻塞 Functional 仅改变或读取内存值,不触发事件,不占用时钟,通常用于初始化、快速读写或调试
sendMemBackdoorReq(const MemBackdoorReq &req, MemBackdoorPtr &backdoor) bool Backdoor 访问(绕过 cache / pipeline) Backdoor 用于调试或直接访问内存,不触发缓存、总线或事件,可以访问内部状态
sendTimingReq(PacketPtr pkt) bool Timing 访问请求,非阻塞 Timing:遵循时钟与延迟,事件驱动 精确 cycle 模拟,建模真实时序(CPU、缓存、网络)
tryTiming(PacketPtr pkt) const bool 尝试发送 Timing 请求,如果资源被占用则返回失败 Timing 在非阻塞状态下尝试发送请求,通常用于流水线中尝试访问共享资源
sendTimingSnoopResp(PacketPtr pkt) bool Timing snoop 响应,非阻塞 Timing Snoop 用于缓存一致性协议,响应其他 cache 或主存 snoop 请求

  • Atomic(sendAtomic)
    • 阻塞 CPU/模块直到访问完成。
    • 不触发事件队列,速度快。
    • 模型行为正确性,但不考虑 cycle。
  • Functional(sendFunctional)
    • 功能性访问,不影响时序。
    • 直接修改或读取数据。
    • 常用于初始化内存、调试工具或外部测试。
  • Timing(sendTiming / tryTiming)
    • 非阻塞,事件驱动,遵循 cycle 精度。
    • 会考虑总线延迟、缓存延迟、争用等。
    • 精确模拟真实硬件时序。
  • Atomic 简单粗暴,不考虑 cycle。
  • Timing 需要事件驱动调度,更接近真实硬件时序。
  • Functional 不触发任何事件,直接改内存。
sendTimingReq  -> TimingRequestProtocol::sendReq   -> TimingResponseProtocol* -> 
recvTimingReq  -> handleRequest -> accessTiming -> sendResponse -> sendPacket -> 
sendTimingResp -> TimingResponseProtocol::sendResp -> TimingRequestProtocol* -> 
recvTimingResp -> handleRespPacket -> handleResp
sendFunctional -> FunctionalRequestProtocol::send -> 
recvFunctional -> accessFunctional -> makeResponse

gem5 基础概念与对应关系

gem5 概念 作用 / 含义 SystemC 对应 Verilog 对应 硬件对应 抽象层级
System 顶层仿真系统对象,包含 CPU、内存、总线 顶层模块 SC_MODULE 顶层 RTL module SoC / Chip 高抽象
CPU 模拟 CPU 核心行为 Module + Process Module / always / RTL 处理器核心 时序抽象 / 可模拟 cycle
Cache 模拟缓存行为 Module + Signal / Process Module + memory array SRAM cache 可抽象 / 可建模存储逻辑
Port 模块间通信接口(Master/Slave) sc_port / sc_in / sc_out input/output/inout 总线/信号端口 抽象线网
Packet 请求/响应封装 Transaction / Packet struct N/A Memory request / bus transaction 抽象访问
EventQueue / Event 时钟和事件调度 sc_event / wait #delay, @(posedge clk) 时钟/延迟/同步逻辑 抽象调度机制
sendTimingReq / recvTimingReq Timing 模型请求 / 响应 send → MasterPort / recv → SlavePort send → master RTL / receive → slave RTL 总线/Cache访问 Timing 模型抽象,非真实并行
sendAtomic / sendFunctional 非阻塞 / 阻塞内存访问 Functional call / wait() blocking / non-blocking assignment Load/Store立即执行 高抽象 / 忽略 cycle 或事件
Stats / Trace 统计/调试信息 N/A N/A 测试与验证 仿真辅助,软件抽象
MinorCPU / O3CPU / TimingSimpleCPU 不同 CPU 模型 Process/Thread 模块 RTL CPU pipeline Pipeline / Stage / Core 不同精度抽象
SimObject 基类,所有仿真对象继承 SC_MODULE / object module / struct 逻辑单元 仿真抽象对象

DMA指令执行

gem5 中,执行一条 DMA 指令涉及多个组件和类的交互,主要包括 CPU、DMA 控制器、内存子系统和事件队列等。下面是一个完整的 DMA 指令执行流程示例,以说明数据流转和请求在 gem5 中经过的关键类。

假设场景

假设 CPU 发出一条 DMA 请求,要求将一段数据从内存搬运到设备(或从设备搬运到内存)。以下是 gem5 中主要涉及的类及其作用:

1. CPU 发出 DMA 请求

  • 涉及类X86CPUThreadContext
  • 过程
  • CPU 代码(例如 X86CPU 中)识别到 DMA 请求指令,通过 ThreadContext 获取当前的请求上下文(如数据地址、大小等)。
  • 然后 CPU 会创建一个 DMA 请求,并将请求信息传递给 DMA 控制器。

2. DMA 控制器接收请求

  • 涉及类DMACtrlDmaPort
  • 过程
  • DMACtrl 类作为 DMA 控制器的主要实现类,接收到请求后,负责处理数据搬运的具体流程。
  • DMACtrl 通过其 DmaPort 端口与系统总线或内存子系统通信,将数据请求发送到内存。
  • DMACtrl 会在 EventQueue 中注册一个事件,设定为下一步数据搬运操作的触发时间。

3. 数据包的生成

  • 涉及类DmaPortPacket
  • 过程
  • DmaPort::trySendTimingReq() 方法中,DmaPort 创建一个 Packet 对象,封装了 DMA 请求的数据包。
  • Packet 类用于表示内存传输的实际数据块,包括地址、命令类型(读/写)和传输大小等信息。
  • DmaPort 会调用 sendTimingReq(pkt),将 Packet 发送到总线或内存控制器。

4. 发送请求到总线

  • 涉及类MemCtrlBusSystemBus
  • 过程
  • DmaPortPacket 发送给 Bus(系统总线)或 MemCtrl(内存控制器)。
  • Bus 负责在不同设备(如 DMA 控制器、内存、CPU 等)之间传输 Packet,并确保数据请求按时送达内存控制器。

5. 内存控制器处理请求

  • 涉及类MemCtrlDRAMCtrlRubySystem
  • 过程
  • 内存控制器 MemCtrlDRAMCtrl 收到 DMA 请求后,检查数据地址,判断该请求是否能够立即处理。
  • 内存控制器可能会产生延迟,例如访问 DRAM 时会加入行、列访问延迟。
  • 内存控制器将数据读出或写入到指定的内存地址后,通过 Packet 回传结果给 DMA 控制器。

6. DMA 控制器接收数据并完成传输

  • 涉及类DMACtrlDmaPortEventQueue
  • 过程
  • 当内存控制器完成数据传输后,Packet 会返回到 DmaPort,然后再返回到 DMACtrl
  • DMACtrl 检查是否完成所有数据块的搬运。
  • 如果数据搬运尚未完成,DMACtrl 会继续在 EventQueue 中注册新的事件,并在未来的 Tick 触发下一次数据块的传输。
  • 当所有数据传输完成后,DMACtrl 将通过 EventQueue 向 CPU 发送一个完成通知(通常是中断),告知 CPU DMA 操作已完成。

7. CPU 处理 DMA 完成通知

  • 涉及类X86CPUInterrupts
  • 过程
  • CPU 通过中断子系统 Interrupts 接收到 DMA 完成的中断通知。
  • CPU 的中断处理程序开始运行,确认 DMA 操作已完成,并处理后续操作(如读取搬运的数据或继续执行程序)。

流程总结

  • 主要数据流CPUDMACtrlDmaPortBusMemCtrlDmaPortDMACtrlCPU
  • 关键类
  • DMACtrl:管理 DMA 操作的控制器。
  • DmaPort:DMA 控制器与其他组件通信的端口。
  • Packet:封装数据传输请求和响应的数据包。
  • MemCtrl:内存控制器,处理实际的读/写请求。
  • Bus:连接 DMA 控制器和内存控制器的总线。
  • EventQueue:调度 DMA 控制器和其他组件之间的事件,确保各组件按时间顺序工作。

通过上述机制,gem5 实现了 DMA 指令的异步数据流转和各组件间的精确同步,所有操作在单线程中按事件驱动执行。

CPU / MasterPort
    │
    │ sendTimingReq(pkt)
    ▼
SlavePort (Cache / Memory)
    │
    │ recvTimingReq(pkt)  ←── 处理请求
    │     │
    │     └─> 处理完毕后,生成响应 pkt
    │
    │ sendTimingResp(pkt)
    ▼
MasterPort / CPU
    │
    │ recvTimingResp(pkt)  ←── 收到响应
    ▼
CPU / 上层模块处理数据
MasterPort 发起请求 → 调用 sendTimingReq()

SlavePort 接收请求 → recvTimingReq()

SlavePort 处理请求 → 内部事件 process()

处理完成生成响应 → 调用 sendTimingResp()

MasterPort 接收响应 → recvTimingResp() → 上层 CPU / 模块更新寄存器或状态

Pipeline

Fetch1

这是 MinorCPU 里取指一阶段(Fetch1)的主逻辑。它每个周期被调用一次,完成分支重定向判断、为下游预留槽位、发起取指请求、推进 icache 事务队列、接收并输出取回的指令行。按代码走一遍:

它先读入来自其他阶段的“控制线”

  • execute_branch = *inp.outputWire:来自 Execute(后段)的分支/流切换信息(真实执行得到的分支/异常/redirect)。
  • fetch2_branch = *prediction.outputWire:来自 Fetch2 的分支预测结果(前段预测)。
  • line_out = *out.inputWire:当前要输出给下游(Fetch2)的取指行(初始应是 bubble)。

1) 标记可否向下游发(是否被下游堵住)

for each tid:
  fetchInfo[tid].blocked = !nextStageReserve[tid].canReserve();

  • 每个线程各有一个“下游槽位预留器”。不能预留就视为被阻塞。

2) 处理分支与“流切换(changeStream)”

按线程维度处理 ExecuteFetch2 预测 两条分支线:

  • 如果两条分支都有效且指向同一线程
    • 优先响应 Execute(真实)分支:若 isStreamChange() 且线程未 Halt,则 changeStream(execute_branch)
    • 若同时有预测分支,打印“忽略预测”的调试信息(真实分支优先)。
    • 否则,若真实分支不需要切流、但预测需要,且预测的 newStreamSeqNum 与线程当前 streamSeqNum 匹配,则执行 changeStream(fetch2_branch)
  • 如果来自不同线程
    • 对各自线程分别判断是否需要 changeStream(...)(同样要考虑线程是否 Halt、以及预测的 streamSeqNum 是否匹配)。
这里的 streamSeqNum 是“取指流代号”,用来丢弃旧流上的迟到请求/响应,避免乱序/陈旧数据污染。

3) 若在飞取指次数未达上限,选择一个线程发起取指

if (numInFlightFetches() < fetchLimit) {
  tid = getScheduledThread();
  if (tid != Invalid) {
    fetchLine(tid);                 // 生成并发出一个 I-cache 行取指请求
    nextStageReserve[tid].reserve();// 下游预留一个槽位
  }
}

  • fetchLimit:控制最多挂起多少条取指。
  • getScheduledThread():选择下一个要取指的 SMT 线程(调度策略)。
  • fetchLine(tid):根据当前 PC/流状态构造取指行请求(I-cache miss 则走访存)。
  • 成功发出后为该线程在下游队列预留位置(避免过度生产)。

4) 推进端口/队列:让在飞的请求继续流动

stepQueues();

  • 驱动 icachePort 的发送/返回队列,与内存系统继续握手(把 outstanding 的事件推进一拍)。

5) 若有已完成的取指行(transfer),消费它

if (!transfers.empty() && transfers.front()->isComplete()) {
  auto response = transfers.front();
  if (response->isDiscardable()) {
    nextStageReserve[tid].freeReservation(); // 释放之前预留的槽位
    cpu.wakeupOnEvent(Pipeline::Fetch1StageId); // 提醒下一拍重试
  } else {
    processResponse(response, line_out); // 把取回的指令行打到 line_out 输出
  }
  popAndDiscard(transfers); // 出队
}

  • 完成的响应若 属于旧的流(discardable),直接丢弃并释放预留槽位;否则调用 processResponse 生成对下游的输出。
  • 这与前面的 streamSeqNum 机制配合,保证 redirect 后旧流迟到数据被丢弃。

6) 若本拍产生了输出,记录流水线活动

if (!line_out.isBubble())
  cpu.activityRecorder->activity();

  • 标记该阶段“有活”,利于节能/统计。

小结(做了哪些事)

  1. 读分支线(真实/预测),必要时切换取指流(带 streamSeqNum 防旧流污染)。
  2. 看下游是否能接,并在 in-flight 限制内选择线程发起取指、为下游预留槽位
  3. 推进 I-cache/内存队列,接收已完成的取指行:
    • 属于旧流 → 丢弃并释放预留
    • 属于当前流 → 产出 line_out 给 Fetch2
  4. 记录活动以推进流水线。

这就是 MinorCPU 取指一级(Fetch1)的“发/收/重定向/背压/过滤陈旧响应”的完整工作流。

Fetch2

这是 MinorCPU 的取指第二阶段(Fetch2)主循环。它把 Fetch1 送来的“指令行”分解成具体指令、做本地分支预测、并把解出的动态指令打包给下游。按代码路径分解如下:

它先做的输入整理

  • 把来自 Fetch1 的一条 ForwardLineData(若非 bubble)塞进对应线程的输入环形缓冲inputBuffer[tid].setTail(...)
  • 取本阶段的输出句柄 insts_out(起始是 bubble),以及两路分支控制:
    • branch_inp:来自 Execute 的真实分支/redirect;
    • prediction:本阶段将要产生并输出给 Fetch1 的预测分支

1) 先处理来自 Execute 的真实分支

  • updateBranchPrediction(branch_inp):用真实分支结果训练/更新本地预测结构。
  • branch_inp.isStreamChange()(发生真实流切换),丢弃该线程所有输入行并将 havePC=false(等待新 PC 重新对齐)。

2) 清理“预测序列号不匹配”的陈旧行

  • 对每个线程:
    • 标记下游是否能接收:thread.blocked = !nextStageReserve[tid].canReserve()
    • 从输入缓冲看顶端行 line_in,在流序号(streamSeqNum)一致预测序号(predictionSeqNum)不一致时,循环丢弃这些行,并清 havePC=false

      (保证 redirect 或重预测后,旧预测生成的行不会被错误解出。)

3) 选择一个线程做本拍取指解码

  • tid = getScheduledThread():选择一个可用线程(SMT 调度策略)。
  • 若有可用线程,进入主循环把输入行“解成指令”,直到:
    • 当前输入行用完 / 需要更多字节但没行 / 已达本拍输出上限 outputWidth / 已产生一个分支预测 prediction

在这段循环里做的事

  • 取该线程的 decoderfetch_info(PC/序列号/行内偏移等状态)。
  • 必要时设置 PC 与行内偏移
    • 若切换了流或此前没有 PC:根据 line_in->pc 和解码器的 pcMask() 计算 inputIndex(该 PC 在本行中的字节偏移),设置 fetch_info.pcdecoder->reset()
  • 判断是否需要丢弃整行(同流但预测序号不匹配);
  • line_in->isFault():构造一条 fault dyn_inst(携带 fault),并继续允许生成其他指令。
  • 否则执行解码
    • 把当前行字节拷给解码器 moreBytesPtr();若不够,调用 decoder->moreBytes(...)
    • 如果 instReady()
      • decoded_inst = decoder->decode(*pc),构造 MinorDynInst,设置 fetchSeqNum/predictionSeqNum,拷贝 PC;
      • 统计指令类型(load/store/atomic/vector/fp/int);
      • 微 PC 处理pc->uReset(),确保随后的 advancePC 从宏指令起点计;
      • decoded_inst->advancePC(*pc):推进到下一条;
      • predictBranch(dyn_inst, prediction)对该指令做分支预测,必要时填充 prediction(一旦产生预测,本拍停止再取)。
    • 如果解码器还需要更多字节,按 moreBytesSize() 推进 inputIndex
  • 若本条产生了 dyn_inst
    • fetchSeqNum++;若这是本拍第一条,insts_out.resize(outputWidth)
    • dyn_inst 放到 insts_out.insts[output_index++];必要时输出 MinorTrace。
  • 维护流变更跟踪:lastStreamSeqNum = line_in->id.streamSeqNum
  • 行管理与丢弃策略
    • 如果已产生 prediction 或本行是 fault:丢弃所有输入havePC=false
    • 若只是“预测序号不匹配导致丢行”:仅弹出该行havePC=false
    • inputIndex == lineWidth:本行解析完,弹出该行(PC 保留,防行跨界的指令)。
  • 若允许跨多行处理(processMoreThanOneInput),空行时会继续取下一行循环。

4) 输出与资源预留

  • 始终把本阶段的预测结果送出:predictionOut.inputWire = prediction(可能是 bubble)。
  • 如果本拍生成了任何指令(!insts_out.isBubble()):
    • 标记活动:cpu.activityRecorder->activity()
    • 设置 insts_out.threadId = tid
    • 在下游预留一个槽位:nextStageReserve[tid].reserve()
  • 若仍有可处理的输入且下游能接,激活本阶段:activateStage(Fetch2StageId)
  • 把刚塞入的输入行推尾到缓冲:inputBuffer[tid].pushTail()(确保缓冲前进)。

这一阶段“做了哪些事”的要点清单

  1. 训练预测器:用后段真实分支结果更新本地 BP。
  2. 响应真实 redirect:来真实分支就清空输入、等待新流 PC。
  3. 清理陈旧预测行:在相同流号下,丢弃预测序号不匹配的行。
  4. 选线程并解码:从输入行按 PC 偏移喂解码器,生成 MinorDynInst
  5. 产生分支预测:对解出的指令调用 predictBranch,一旦预测,终止本拍继续解。
  6. 故障行处理:fault 行包装为 fault 指令输出。
  7. 行/PC 维护:处理跨行、行尾弹出、流号跟踪、微 PC 复位。
  8. 统计与输出:记录基本指令类别,打包到 insts_out,给下游预留槽位并标记活动。

它与 Fetch1 配合:Fetch1 负责取行与流切换,Fetch2 负责解码成指令与预测下一跳,并用 streamSeqNum/predictionSeqNum 机制保证 redirect 后的陈旧数据不外泄

Decode

这是 MinorCPU 的 Decode 阶段主循环。它把 Fetch2 产出的“动态指令包”进一步微指令分解(macro→micro)、分配执行序号、并把结果推给下游。按执行顺序说明:

输入与阻塞探测

  • 若来自上游 inp 不是 bubble,则按线程把该批 ForwardInstData 放入对应 inputBuffer[tid] 的尾部(setTail)。
  • 取到本阶段输出 insts_out(初始是 bubble)。
  • 为每个线程标记是否被下游阻塞:decodeInfo[tid].blocked = !nextStageReserve[tid].canReserve()
  • 选择一个可执行线程tid = getScheduledThread()(SMT 选择策略)。

取指令并解码/分解

若选到线程:

  • insts_in = getInput(tid) 取该线程输入批;output_index 计数输出槽位。
  • 循环装填输出,条件:仍有输入元素、未填满 outputWidth
  • 逐条查看 inst = insts_in->insts[inputIndex]
    • bubble:跳过,inputIndex++,并清 inMacroop=false
    • fault 指令:只是“传递”错误,不分解;inputIndex++,清 inMacroop
    • 非 fault
      • static_inst = inst->staticInst,默认 output_inst = inst
      • 如果是 macro-op(需要拆微指令):
        • 若刚进入一个宏指令,初始化 microopPC = inst->pc,置 inMacroop=true
        • 通过 static_inst->fetchMicroop(microopPC->microPC()) 取一条 micro-op 的 StaticInst
        • 构造新的 MinorDynInst output_inst(携带原 id/流信息),设其 pc = microopPC
        • 只有在 最后一条 micro-op 时,才把 分支预测位与目标predictedTaken/Target)从宏指令继承到该 micro-op(避免在中间 micro-op 上带预测)。
        • static_micro_inst->advancePC(*microopPC) 前进微 PC。
        • 若已是最后一条 micro-op:inputIndex++inMacroop=false(完成对该宏指令的分解)。
      • 如果 不是 macro-op:不分解,直接“传递”该动态指令;inputIndex++inMacroop=false
      • 统一处理:
        • 赋执行序号:output_inst->id.execSeqNum = decode_info.execSeqNum; decode_info.execSeqNum++
        • 若是本批第一条,insts_out.resize(outputWidth) 以便写入。
        • 写入 insts_out.insts[output_index++] = output_inst
  • 批内输入耗尽时(inputIndex == insts_in->width()):
    • 断言不在宏指令中(!inMacroop),弹出该输入批 popInput(tid)
    • 若允许一次处理多批(processMoreThanOneInput),则“wrap”再取下一批继续。

输出、活跃与推进

  • 若本拍确实产生了输出(!insts_out.isBubble()):
    • 记录流水线活动 activity()
    • 标注输出所属线程 insts_out.threadId = tid
    • 为下游预留一个槽位nextStageReserve[tid].reserve()
  • 若还有未处理的输入且下游能接收,激活本阶段:activateStage(Pipeline::DecodeStageId)(提示下一拍继续干活)。
  • 最后,把刚放到尾部的输入批真正入队inputBuffer[tid].pushTail()(确保队列推进)。

这个阶段的“关键职责”小结

  1. 输入批到线程缓冲:把 Fetch2 的批次按线程入队。
  2. SMT 选择 + 背压:基于 nextStageReserve 感知下游容量,选择活跃线程。
  3. 宏指令分解:把 macro-op 拆成若干 micro-ops;仅在最后一个 micro-op上传递分支预测信息。
  4. 执行序号编排:为每条输出指令分配单调递增的 execSeqNum
  5. 批内迭代与多批包容:在不阻塞且未达 outputWidth 前,尽可能用完输入;支持一次吞多批。
  6. 产出与资源预留:写 insts_out,为下游预留槽位并记录活动,维持流水线流动性。

Execute

这是 MinorCPU 的 Execute 阶段主循环。它在每拍里完成:把上游指令入队、检查中断、提交(Commit)、发射(Issue)、推进功能单元与 LSQ、生成分支/停取指控制、决定是否下拍继续唤醒。按代码顺序梳理:


入口与输入

  • 若上游 inp 不是 bubble,则按线程把该批 ForwardInstData 放入本阶段 inputBuffer[tid] 尾部(setTail)。
  • 准备本阶段输出 BranchData &branch(初始是 bubble)。
  • num_issued=0 统计本拍发射条数。

1) 先推进内存侧(避免背压)

  • lsq.step();:推进数据缓存端口/LSQ 状态,可能释放 LSQ 请求队列的空间。

2) 处理中断优先

  • checkInterrupts(branch, interrupted)
    • 如有待处理的中断,返回 interrupt_tid 并在 branch 中填入中断引发的控制流变化;本拍不再对其它线程发射/提交
  • 若没有中断,但 branch 早已有旧分支(非 bubble),本拍让出一拍以便该分支完成(打印 DPRINTF 并跳过发射/提交)。

3) 正常提交与发射

  • 选择可提交线程:commit_tid = getCommittingThread()

    若存在:

    • 取其上下文 commit_info,按 drain 状态分三种:
      • DrainCurrentInst:只提交当前宏指令内的 micro-ops(不杀后续),若提交后“处于指令间隙”,切到 DrainHaltFetch;丢弃本拍生成的分支 branch=bubble()
      • DrainAllInsts:清空该线程所有输入(popInput 直到空),然后执行一次 commit(commit_tid, /*only_microops=*/false, /*kill_all=*/true, branch)
      • NotDraining:若处于中断且该线程仍有中断挂起,则只提交 micro-ops;否则正常提交(commit(..., only_commit_microops, false, branch))。
    • 若刚进入 DrainHaltFetch:构造一个 HaltFetch 分支更新(updateBranchData(..., BranchData::HaltFetch, ...)),唤醒执行阶段并把 drain 状态切到 DrainAllInsts(等待清空)。
  • 选择可发射线程:issue_tid = getIssuingThread()

    若存在:num_issued = issue(issue_tid);(即使有中断挂起也允许发射,本着“之后中断会改变流”的假设)。

4) 推进功能单元流水并评估下一拍是否仍需活跃

  • 收集各线程“下一条可发射指令”到 next_issuable_insts,初步标志 can_issue_next=false
  • 遍历所有功能单元流水线:
    • pipeline->advance(); 推进一拍。
    • 若流水线占用且未阻塞,becoming_stalled=false(说明还有活可以动)。
    • 试投下一条:若流水线不堵、类型匹配(provides(opClass))、scoreboard 允许、时序/资源可行且 canInstIssue(...),则 can_issue_next=true
  • 检查在飞指令队列头是否可被提交,若任一线程 inFlightInsts 非空,则 FUs_need_to_update=true

5) 计算是否需要唤醒下一拍

  • 组合条件:
    need_to_tick =
       (本拍发射过) ||
       (功能单元仍能前进) ||
       (下一拍可发射) ||
       (可能可提交) ||
       (LSQ 需要前进) ||
       (有中断在处理)
    
    
  • need_to_tick,调用 cpu.wakeupOnEvent(Pipeline::ExecuteStageId) 约定下一拍继续调度本阶段。

6) 活动标记与推进输入游标

  • 若本拍 branch 非 bubble(产生了控制流更新,如中断/Drain/HaltFetch/分支),记一次流水线活动:cpu.activityRecorder->activity()
  • 若入口有输入(非 bubble),将之前 setTail 的项真正推入队列:inputBuffer[tid].pushTail()

总结:本函数一拍内完成的工作

  1. LSQ/内存侧推进,先消背压。
  2. 中断最高优先级:生成 branch,暂停其它线程提交/发射。
  3. 提交阶段:按 drain 状态/中断状态选择仅提交 micro-ops、清空或正常提交;必要时发出 HaltFetch 控制。
  4. 发射阶段:从可发射线程取指令给功能单元。
  5. 功能单元推进与活跃性判断:前进流水、评估是否还有可发射/可提交/LSQ 需要推进,从而决定是否唤醒下一拍
  6. 输出与 bookkeeping:如有分支/控制输出,记录活动;推进输入缓冲。

🧩 逻辑组成与顺序

按执行顺序,Execute 阶段每拍会依次完成以下逻辑块:

① 前期维护与输入处理

  • 把上游 Decode 阶段输出的指令包放入本阶段 inputBuffer
  • 调用 lsq.step() 推进 Load/Store Queue(模拟访存端口进展),释放请求槽位;
  • 初始化计数变量与输出分支 branch

中断检查(Interrupt check)

  • 调用 checkInterrupts(branch, interrupted)
    • 若有中断触发,则:
      • 生成 BranchData(表示“跳转到中断入口”);
      • 暂停本拍后续发射与提交;
      • 优先处理中断线程(其它线程不动)。
🔹这一块逻辑相当于: 在 issue/commit 前先做异常检测 → 对应乱序 CPU 的“interrupt flush + redirect”。

Commit 阶段逻辑

  • 选中一个可提交线程:commit_tid = getCommittingThread()
  • 调用 commit(commit_tid, ...) 完成提交操作;
  • 根据 drainState(CPU 清空状态)分支:
    • DrainCurrentInst: 只提交当前宏指令中的 micro-ops;
    • DrainAllInsts: 清空整个线程的在飞指令;
    • NotDraining: 正常提交(含分支、异常、访存完成的指令)。
  • DrainHaltFetch 状态出现,发出 HaltFetch 分支更新信号给取指阶段(阻止后续 Fetch)。
🔹对应传统流水线的 Commit/Writeback 阶段。

MinorCPU 没有独立的 Reorder Buffer,而是顺序地在 Execute 中调用 commit 来实现。


Issue 阶段逻辑

  • 选出可发射线程:issue_tid = getIssuingThread()
  • 调用 issue(issue_tid)
    • 检查该线程最前端指令依赖、资源、scoreboard;
    • 若可发射,则将其送入相应功能单元(ALU/FPU/LoadStore 等);
    • 计数 num_issued++
🔹这对应经典流水线的 Issue + Dispatch + Execute 准备阶段。

功能单元执行与调度推进

  • 遍历所有功能单元(funcUnits)与其内部 pipeline:
    • 调用 pipeline->advance() 推进执行;
    • 若 pipeline 中仍有占用但未阻塞,则记录本拍不“stalled”;
    • 检查是否能在下一拍发射更多指令(预测 can_issue_next)。
  • 检查在飞指令(inFlightInsts)是否仍需更新 → 影响是否保持活动。
🔹这相当于多周期 Execute 阶段推进,带延迟执行、功能单元 busy 状态。

活动检测与唤醒机制

  • 综合:
    • 本拍是否发射过;
    • 功能单元是否仍可前进;
    • 是否有下一拍可发射;
    • 是否有在飞指令待提交;
    • LSQ 是否需要继续推进;
    • 是否有未处理中断。
  • 若任一为真,则 cpu.wakeupOnEvent(Pipeline::ExecuteStageId) 保证下一拍继续调度。
🔹这对应“流水线活动保持”机制。

若完全空闲(stall 且无事件),则进入休眠,直到上游或中断唤醒。


输出与收尾

  • 若本拍生成分支(branch != bubble()),标记活动;
  • 推进输入队列尾指针(inputBuffer.pushTail())。

🧠 总结:阶段融合顺序

执行阶段子功能 执行顺序 对应传统流水阶段 关键函数
1️⃣ 输入入队 + LSQ 前进 最先 Memory / Pre-issue lsq.step()
2️⃣ 中断检测 早期 Exception / Flush checkInterrupts()
3️⃣ Commit 提交 其次 Writeback / Retire commit()
4️⃣ Issue 发射 随后 Issue / Dispatch issue()
5️⃣ 功能单元推进 后半 Execute pipeline->advance()
6️⃣ 活动检测与唤醒 收尾 Pipeline control wakeupOnEvent()
7️⃣ 输出分支与清理 最后 Branch / Control updateBranchData()

所以:

✅ MinorCPU 的 Execute 阶段融合了传统 CPU 的 issue → execute → commit 流程。

顺序大致是:

LSQ step → 中断检测 → commit → issue → 功能单元推进 → 活动检测 → 输出分支

与官方MinorCPU差别

✅ 当前版本(你贴的代码)中的行为,和

🕹️ “传统的” MinorCPU(gem5 官方默认的 Minor 模型)到底有何区别。


1. 先说结论
🔹 你看到的这份 Execute 实现,并不是乱序执行(out-of-order)。

它依然是顺序执行(in-order),但:

  • 它允许多个线程并发;
  • 它能让功能单元有流水(multi-cycle execution),看起来像乱序;
  • 它引入了 “scoreboard + pipeline 资源检查”,形成“有限乱序”的效果。

换句话说,它是 “顺序发射 + 异步完成”,而不是传统意义上的乱序执行。


2. “传统的” MinorCPU Execute 阶段代码结构

在 gem5 官方仓库(例如 src/cpu/minor/execute.cc)中,

经典的 MinorCPU 的 Execute::evaluate() 大致逻辑如下(简化伪码):

Execute::evaluate() {
    // 1. 从 Decode 阶段取输入
    if (!inp.outputWire->isBubble())
        inputBuffer[tid].setTail(*inp.outputWire);

    BranchData &branch = *out.inputWire;

    // 2. 处理中断(如果有)
    checkInterrupts(branch);

    // 3. 选出可提交线程,执行 commit()
    ThreadID commit_tid = getCommittingThread();
    if (commit_tid != InvalidThreadID)
        commit(commit_tid, ...);

    // 4. 选出可发射线程,执行 issue()
    ThreadID issue_tid = getIssuingThread();
    if (issue_tid != InvalidThreadID)
        issue(issue_tid);

    // 5. 推进功能单元(模拟执行过程)
    for (auto fu : funcUnits)
        for (auto &pipeline : fu->pipelines)
            pipeline->advance();

    // 6. 若仍有未完成指令或中断待处理,则唤醒下一拍
    if (stillActive())
        cpu.wakeupOnEvent(Pipeline::ExecuteStageId);
}

特点:

项目 说明
顺序执行模型 所有线程共用一个 issue/commit 时隙,保证顺序。
功能单元流水化 功能单元可以多拍完成指令,但同一线程指令不能乱序。
scoreboard 控制 检查寄存器、功能单元占用冲突,确保依赖有序。
无重排序缓冲区(ROB) 不支持真正的乱序提交或投机执行。
多线程切换 可以多线程交替 issue(时间片式多线程)。
所以传统 Minor 模型其实是:

“顺序发射、乱序完成、顺序提交”

——这是 in-order pipeline + pipelined FU 的典型特征。


3. 你贴的版本的不同之处

从你提供的 Execute::evaluate() 代码看,它是经过扩展/优化版的 MinorCPU,主要区别如下:

对比点 传统 MinorCPU 你贴的版本
线程模型 1~2 个线程,轮询发射 支持多线程循环检查,每个线程独立 commit/issue
issue/commit 次序 固定顺序:commit → issue 同样顺序,但对每个线程独立处理
功能单元控制 简单 pipeline + advance() 支持多 pipeline + canInstIssue() 动态决策
scoreboard 全局寄存器依赖表 每线程独立 scoreboard,允许更并行发射
发射逻辑 仅一个线程可 issue 可从多个线程尝试 issue(但每线程仍顺序)
乱序程度 完全顺序执行 支持“跨线程并发、功能单元异步完成”——伪乱序

也就是说,它仍然是 Minor 模型(in-order core)

但行为更接近 Fine-grained multithreading (FGMT),即“细粒度多线程顺序核”。


4. 为什么看起来像“乱序执行”

有三个机制让它表现得像乱序:

机制 效果
功能单元多周期 (pipeline->advance()) 指令执行周期不同,完成顺序可能不同。
多线程发射 多线程并发执行,不同线程的指令交错。
scoreboard 控制 允许无依赖的指令抢先发射(但同线程仍按序)。

所以在 trace 里你可能看到:

Thread 0: inst 1 执行
Thread 1: inst 1 执行
Thread 0: inst 2 等待
Thread 1: inst 2 提交

这看起来像乱序,其实是“线程交错 + 异步执行”的假象。


5. 真正乱序(OoO)CPU 的区别

真正的乱序核(如 gem5 的 O3CPU)包含:

模块 功能
重命名寄存器 (Register Rename) 消除写后读/写后写依赖
指令窗口 / 调度队列 按准备好时间动态 issue
ROB(Reorder Buffer) 按序提交结果
投机执行 / 分支恢复 支持预测执行、回滚恢复

MinorCPU 没有这些,而只是:

  • 检查寄存器占用;
  • 顺序发射;
  • 允许多周期执行。

所以严格来说:

🟢 Minor 是 “顺序执行(in-order) + 异步执行单元”

🔴 O3CPU 才是 “真正乱序(out-of-order)”。


总结一句话
项目 传统 MinorCPU 你贴的版本 真正 OoO CPU
发射顺序 顺序 顺序(跨线程并行) 乱序
提交顺序 顺序 顺序 顺序(通过 ROB)
线程并行 单线程/双线程 多线程独立 issue 多线程支持但复杂
执行单元 多周期顺序 多 pipeline 异步 动态调度
是否乱序 ❌ 否 ⚠️ 类乱序(异步+多线程) ✅ 是